(function(window, document, exportName, undefined) {
	"use strict";

	var isMultiTouch = false;
	var multiTouchStartPos;
	var eventTarget;
	var touchElements = {};

	// polyfills
	if (!document.createTouch) {
		document.createTouch = function(view, target, identifier, pageX, pageY, screenX, screenY, clientX, clientY) {
			// auto set
			if (clientX == undefined || clientY == undefined) {
				clientX = pageX - window.pageXOffset;
				clientY = pageY - window.pageYOffset;
			}

			return new Touch(target, identifier, {
				pageX: pageX,
				pageY: pageY,
				screenX: screenX,
				screenY: screenY,
				clientX: clientX,
				clientY: clientY
			});
		};
	}

	if (!document.createTouchList) {
		document.createTouchList = function() {
			var touchList = new TouchList();
			for (var i = 0; i < arguments.length; i++) {
				touchList[i] = arguments[i];
			}
			touchList.length = arguments.length;
			return touchList;
		};
	}

	/**
	 * create an touch point
	 * @constructor
	 * @param target
	 * @param identifier
	 * @param pos
	 * @param deltaX
	 * @param deltaY
	 * @returns {Object} touchPoint
	 */
	function Touch(target, identifier, pos, deltaX, deltaY) {
		deltaX = deltaX || 0;
		deltaY = deltaY || 0;

		this.identifier = identifier;
		this.target = target;
		this.clientX = pos.clientX + deltaX;
		this.clientY = pos.clientY + deltaY;
		this.screenX = pos.screenX + deltaX;
		this.screenY = pos.screenY + deltaY;
		this.pageX = pos.pageX + deltaX;
		this.pageY = pos.pageY + deltaY;
	}

	/**
	 * create empty touchlist with the methods
	 * @constructor
	 * @returns touchList
	 */
	function TouchList() {
		var touchList = [];

		touchList.item = function(index) {
			return this[index] || null;
		};

		// specified by Mozilla
		touchList.identifiedTouch = function(id) {
			return this[id + 1] || null;
		};

		return touchList;
	}


	/**
	 * Simple trick to fake touch event support
	 * this is enough for most libraries like Modernizr and Hammer
	 */
	function fakeTouchSupport() {
		var objs = [window, document.documentElement];
		var props = ['ontouchstart', 'ontouchmove', 'ontouchcancel', 'ontouchend'];

		for (var o = 0; o < objs.length; o++) {
			for (var p = 0; p < props.length; p++) {
				if (objs[o] && objs[o][props[p]] == undefined) {
					objs[o][props[p]] = null;
				}
			}
		}
	}

	/**
	 * we don't have to emulate on a touch device
	 * @returns {boolean}
	 */
	function hasTouchSupport() {
		return ("ontouchstart" in window) || // touch events
			(window.Modernizr && window.Modernizr.touch) || // modernizr
			(navigator.msMaxTouchPoints || navigator.maxTouchPoints) > 2; // pointer events
	}

	/**
	 * disable mouseevents on the page
	 * @param ev
	 */
	function preventMouseEvents(ev) {
		// 注释启用默认事件
		// ev.preventDefault();
		// ev.stopPropagation();
	}

	/**
	 * only trigger touches when the left mousebutton has been pressed
	 * @param touchType
	 * @returns {Function}
	 */
	function onMouse(touchType) {
		return function(ev) {
			// prevent mouse events
			preventMouseEvents(ev);

			if (ev.which !== 1) {
				return;
			}

			// The EventTarget on which the touch point started when it was first placed on the surface,
			// even if the touch point has since moved outside the interactive area of that element.
			// also, when the target doesnt exist anymore, we update it
			if (ev.type == 'mousedown' || !eventTarget || (eventTarget && !eventTarget.dispatchEvent)) {
				eventTarget = ev.target;
			}

			// shiftKey has been lost, so trigger a touchend
			if (isMultiTouch && !ev.shiftKey) {
				triggerTouch('touchend', ev);
				isMultiTouch = false;
			}

			triggerTouch(touchType, ev);

			// we're entering the multi-touch mode!
			if (!isMultiTouch && ev.shiftKey) {
				isMultiTouch = true;
				multiTouchStartPos = {
					pageX: ev.pageX,
					pageY: ev.pageY,
					clientX: ev.clientX,
					clientY: ev.clientY,
					screenX: ev.screenX,
					screenY: ev.screenY
				};
				triggerTouch('touchstart', ev);
			}

			// reset
			if (ev.type == 'mouseup') {
				multiTouchStartPos = null;
				isMultiTouch = false;
				eventTarget = null;
			}
		}
	}

	/**
	 * trigger a touch event
	 * @param eventName
	 * @param mouseEv
	 */
	function triggerTouch(eventName, mouseEv) {
		var touchEvent = document.createEvent('Event');
		touchEvent.initEvent(eventName, true, true);

		touchEvent.altKey = mouseEv.altKey;
		touchEvent.ctrlKey = mouseEv.ctrlKey;
		touchEvent.metaKey = mouseEv.metaKey;
		touchEvent.shiftKey = mouseEv.shiftKey;

		touchEvent.touches = getActiveTouches(mouseEv, eventName);
		touchEvent.targetTouches = getActiveTouches(mouseEv, eventName);
		touchEvent.changedTouches = getChangedTouches(mouseEv, eventName);

		eventTarget.dispatchEvent(touchEvent);
	}

	/**
	 * create a touchList based on the mouse event
	 * @param mouseEv
	 * @returns {TouchList}
	 */
	function createTouchList(mouseEv) {
		var touchList = new TouchList();

		if (isMultiTouch) {
			var f = TouchEmulator.multiTouchOffset;
			var deltaX = multiTouchStartPos.pageX - mouseEv.pageX;
			var deltaY = multiTouchStartPos.pageY - mouseEv.pageY;

			touchList.push(new Touch(eventTarget, 1, multiTouchStartPos, (deltaX * -1) - f, (deltaY * -1) + f));
			touchList.push(new Touch(eventTarget, 2, multiTouchStartPos, deltaX + f, deltaY - f));
		} else {
			touchList.push(new Touch(eventTarget, 1, mouseEv, 0, 0));
		}

		return touchList;
	}

	/**
	 * receive all active touches
	 * @param mouseEv
	 * @returns {TouchList}
	 */
	function getActiveTouches(mouseEv, eventName) {
		// empty list
		if (mouseEv.type == 'mouseup') {
			return new TouchList();
		}

		var touchList = createTouchList(mouseEv);
		if (isMultiTouch && mouseEv.type != 'mouseup' && eventName == 'touchend') {
			touchList.splice(1, 1);
		}
		return touchList;
	}

	/**
	 * receive a filtered set of touches with only the changed pointers
	 * @param mouseEv
	 * @param eventName
	 * @returns {TouchList}
	 */
	function getChangedTouches(mouseEv, eventName) {
		var touchList = createTouchList(mouseEv);

		// we only want to return the added/removed item on multitouch
		// which is the second pointer, so remove the first pointer from the touchList
		//
		// but when the mouseEv.type is mouseup, we want to send all touches because then
		// no new input will be possible
		if (isMultiTouch && mouseEv.type != 'mouseup' &&
			(eventName == 'touchstart' || eventName == 'touchend')) {
			touchList.splice(0, 1);
		}

		return touchList;
	}

	/**
	 * show the touchpoints on the screen
	 */
	function showTouches(ev) {
		var touch, i, el, styles;

		// first all visible touches
		for (i = 0; i < ev.touches.length; i++) {
			touch = ev.touches[i];
			el = touchElements[touch.identifier];
			if (!el) {
				el = touchElements[touch.identifier] = document.createElement("div");
				document.body.appendChild(el);
			}

			styles = TouchEmulator.template(touch);
			for (var prop in styles) {
				el.style[prop] = styles[prop];
			}
		}

		// remove all ended touches
		if (ev.type == 'touchend' || ev.type == 'touchcancel') {
			for (i = 0; i < ev.changedTouches.length; i++) {
				touch = ev.changedTouches[i];
				el = touchElements[touch.identifier];
				if (el) {
					el.parentNode.removeChild(el);
					delete touchElements[touch.identifier];
				}
			}
		}
	}

	/**
	 * TouchEmulator initializer
	 */
	function TouchEmulator() {
		if (hasTouchSupport()) {
			return;
		}

		fakeTouchSupport();

		window.addEventListener("mousedown", onMouse('touchstart'), true);
		window.addEventListener("mousemove", onMouse('touchmove'), true);
		window.addEventListener("mouseup", onMouse('touchend'), true);

		window.addEventListener("mouseenter", preventMouseEvents, true);
		window.addEventListener("mouseleave", preventMouseEvents, true);
		window.addEventListener("mouseout", preventMouseEvents, true);
		window.addEventListener("mouseover", preventMouseEvents, true);

		// it uses itself!
		window.addEventListener("touchstart", showTouches, true);
		window.addEventListener("touchmove", showTouches, true);
		window.addEventListener("touchend", showTouches, true);
		window.addEventListener("touchcancel", showTouches, true);
	}

	// start distance when entering the multitouch mode
	TouchEmulator.multiTouchOffset = 75;

	/**
	 * css template for the touch rendering
	 * @param touch
	 * @returns object
	 */
	TouchEmulator.template = function(touch) {
		var size = 0;
		var transform = 'translate(' + (touch.clientX - (size / 2)) + 'px, ' + (touch.clientY - (size / 2)) + 'px)';
		return {
			position: 'fixed',
			left: 0,
			top: 0,
			background: '#fff',
			border: 'solid 1px #999',
			opacity: .6,
			borderRadius: '100%',
			height: size + 'px',
			width: size + 'px',
			padding: 0,
			margin: 0,
			display: 'block',
			overflow: 'hidden',
			pointerEvents: 'none',
			webkitUserSelect: 'none',
			mozUserSelect: 'none',
			userSelect: 'none',
			webkitTransform: transform,
			mozTransform: transform,
			transform: transform,
			zIndex: 100
		}
	};

	// export
	if (typeof define == "function" && define.amd) {
		define(function() {
			return TouchEmulator;
		});
	} else if (typeof module != "undefined" && module.exports) {
		module.exports = TouchEmulator;
	} else {
		window[exportName] = TouchEmulator;
	}
	TouchEmulator();
})(window, document, "TouchEmulator");
